package com.stottlerhenke.simbionic.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.StringTokenizer;

import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.html.HTMLEditorKit;

import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.Doc;
import com.sun.javadoc.DocErrorReporter;
import com.sun.javadoc.Doclet;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Tag;

/**
 * ClassSpecificationDoclet generate specification files from java source. You can invoke
 * the doclet using javadoc:
 * <br>
 * <code>% javadoc -doclet com.stottlerhenke.util.ClassSpecificationDoclet -docletpath others.jar  MyClass.java </code>
 * <br>
 * Or configure the doclet when exporting javadoc in Eclipse.
 * <br>
 * Please see <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/javadoc/overview.html">Doclet Overview</a>
 * and <a href="http://java.sun.com/j2se/1.4.2/docs/tooldocs/javadoc/doclet/index.html">Doclet API</a>
 * for more information.
 * <br>
 * Accpetable options includes:
 * <br>
 * <li>-d						output directory. The specified directory's path must be surrounded 
 * by quotation marks(") if it contains any spaces, e.g. "c:/my projects/".</li>
 * <li>-docfile					output file, if not specified, output specification files to 
 * subdirectories in source's package stucture</li>
 * <li>-noinheritedmethods		do not output inherited methods</li>
 * <li>-inheritedmethodsonly	output only inherited methods</li>
 * <li>-showinheritancesource	show inheritance source for each inherited method</li>
 * <li>-nohtmltag				remove all html tags in comments</li>
 * <li>-classconfig				specify class configuration file. a class configuration file 
 * describes lists methods allowed or disallowed to be output. The specified file's path must be 
 * surrounded by quotation marks(") if it contains any spaces, e.g. "c:/my projects/config.txt".</li>
 * <li>-outputpackage			packages that are to be output. To get inherited methods' comments, 
 * you will have to include more packages than you actually want. For example, you want to generate
 * specification files for package com.stottlerhenke.a, but some of its classes have superclasses
 * in package com.stottlerhenke.b, you have to load both packages at javadoc command line, and use
 * this option to specify output package as com.stottlerhenke.a. If you have more than one
 * package, separated them by semi-colon ";".</li>
 * <li>-nocomment				no comment</li>
 * 
 *
 */
//* <li>-docfilessubdirs			output specification files to subdirectories in source's 
//* package stucture</li>
public class ClassSpecificationDoclet extends Doclet{
	
	public ClassSpecificationDoclet(RootDoc root) {
	     this.root = root;
	     String[][] options = root.options();
	     outputRoot="";
	     this.usePackageAsFolder = true;
	     this.noInheritedMethods = false;
	     this.noLocalMethods = false;
	     this.showInheritance = false;
	     this.noHTMLTag = false;
	     this.noComment = false;
	     configurations=new HashMap();
	     packages = null;
	     
	     //init options
	     for (int i = 0; i< options.length; i++){
	    	 System.out.println(options[i][0]);
	    	 if (options[i][0].trim().equals("-d")){
	    		 if (options[i][1].startsWith("\"")&&options[i][1].endsWith("\""))
	    			 outputRoot = options[i][1].substring(1, options[i][1].length()-1);
	    		 else
		    		 outputRoot = options[i][1];
	    	 }
//	    	 if (options[i][0].trim().equals("-docfilessubdirs")){
//	    		 usePackageAsFolder = true;
//	    	 }
	    	 if (options[i][0].trim().equals("-docfile")){
	    		 usePackageAsFolder = false;
	    		 outputFilename = options[i][1];
	    	 }
	    	 if (options[i][0].trim().equals("-noinheritedmethods")){
	    		 noInheritedMethods = true;
	    	 }	
	    	 if (options[i][0].trim().equals("-inheritedmethodsonly")){
	    		 noLocalMethods = true;
	    	 }
	    	 if (options[i][0].trim().equals("-showinheritancesource")){
	    		 showInheritance = true;
	    	 }
	    	 if (options[i][0].trim().equals("-nohtmltag")){
	    		 noHTMLTag = true;
	    	 }
	    	 if (options[i][0].trim().equals("-nocomment")){
	    		 noComment = true;
	    	 }
	    	 if (options[i][0].trim().equals("-classconfig")){
	    		 String configfilename="";
	    		 if (options[i][1].startsWith("\"")&&options[i][1].endsWith("\""))
	    			 configfilename = options[i][1].substring(1, options[i][1].length()-1); 
	    		 try {
					this.loadConfigureFile(new BufferedReader(new FileReader(configfilename)));
				} catch (FileNotFoundException e) {
					e.printStackTrace();
				}
	    	 }
	    	 if (options[i][0].trim().equals("-outputpackage")){
	    		 StringTokenizer st = new StringTokenizer(options[i][1], ";");
	    		 packages = new ArrayList(st.countTokens());
	    		 while (st.hasMoreElements()){
	    			 packages.add(st.nextToken());
	    		 }
	    	 }
	     }
	     
	    //System.out.println("noInheritedMethods = "+noInheritedMethods+"; noLocalMethods = "+noLocalMethods);
	}
	
	/**
	 * Start point of this doclet.
	 * 
	 * @return true all the time
	 */
	private boolean outputSpecificationFiles(){
		ClassDoc[] classes=null;
		String packageRoot = "./";
		
		if (packages==null) classes = root.classes();
		else{
			List classList=null;
			for (int i=0;i<packages.size();i++){
				PackageDoc  pdoc = root.packageNamed(packages.get(i).toString());
				if (classList ==null)
					classList = Arrays.asList(pdoc.allClasses());
				else
					classList.addAll(Arrays.asList(pdoc.allClasses()));
			}
			classes = (ClassDoc[])classList.toArray();
		}
		
		if (!usePackageAsFolder){
			new File(outputRoot).mkdirs();
			File specFile = new File(outputRoot+"/"+outputFilename);
			System.out.println("writing to a single file: " + specFile.getPath());
			try {            	
				writer = new PrintWriter(new FileWriter(specFile));
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		//for each class
        for (int i = 0; i < classes.length; ++i) {
            System.out.println("process "+classes[i]);
            
            //close previous spec file
            if (usePackageAsFolder && writer!=null){
            	writer.flush();
            	writer.close();
            }
            
            //generate spec file path
            if (usePackageAsFolder){
            	PackageDoc pdoc = classes[i].containingPackage();
            	//System.out.println(pdoc.name());
            	packageRoot = pdoc.name().replace('.', '/')+"/";
            	
            	//open spec file to write
            	//System.out.println("pakckage root"+packageRoot);
            	String filename;
            	filename = classes[i].name();
            	
            	new File(outputRoot+"/"+packageRoot).mkdirs();
            	File specFile = new File(outputRoot+"/"+packageRoot+filename+".txt");
            	
            	try {            	
            		writer = new PrintWriter(new FileWriter(specFile));
            	} catch (IOException e) {
            		e.printStackTrace();
            	}
            }
            
			if (!noComment){
				//print class comments 
				String commentText = processComments(classes[i]);
				if (! commentText.trim().equals("")){
					writer.println("/**");
					writer.println(commentText);
					writer.println("*/");
				}
			}
			
			//print class name
			writer.println("class "+classes[i].qualifiedName());
			
			//print constructors
			processConstructor(classes[i]);
			
			//print all  other methods
            processClassDoc(classes[i], new HashSet(), classes[i]);       
            
//            System.out.println("    write to "+specFile.getPath());
            
            writer.println("endclass");
        }  
        
        writer.flush();
        writer.close();
        
        System.out.println("Done!");
        
        return true;
	}

	/**
	 * processes constructors for a class.
	 * 
	 * @param doc	specified class doc
	 */
	private void processConstructor(ClassDoc doc){
		if (noLocalMethods) return ;
		
		ConstructorDoc[] constructors = doc.constructors();
		for (int i=0; i< constructors.length; i++){
			
			if (!noComment){
				//comments for constructor
				String commentText = processComments(constructors[i]);			
				if (! commentText.trim().equals("")){
					writer.println("\t/**");
					writer.println("\t"+commentText);
					writer.println("\t*/");
				}			
			}
			
			//method name
			writer.print("\tmethod "+doc.qualifiedName()+" "+doc.name());
			
			//parameters
			writer.print("(");
			Parameter[] params = constructors[i].parameters();
			for (int ii=0;ii<params.length;ii++){
				if (params[ii].name().equals(""))
					//writer.print(params[ii].type().qualifiedTypeName()+params[ii].type().dimension()+" arg"+ii);
					writer.print(params[ii].type().qualifiedTypeName()+" arg"+ii);
				else
					//writer.print(params[ii].type().qualifiedTypeName()+params[ii].type().dimension()+" "+params[ii].name());
					writer.print(params[ii].type().qualifiedTypeName()+" "+params[ii].name());
				if (ii<params.length-1)
					//writer.print(", ");
					writer.print(",");
			}
			writer.print(")\n");
			writer.println("\n");
		}
	}
	
	/**
	 * processes a class's methods. this method is recursively called to process inherited methods.
	 * 
	 * @param doc				the classes doc to be processed
	 * @param overidenMethods	all overiden methods, at begining it is empty
	 * @param currentDoc		if this method is recursively called, this param always point to
	 * the original classes.
	 */
	private void processClassDoc(ClassDoc doc, HashSet overidenMethods, ClassDoc currentDoc){
		if (doc==null) return;
		System.out.println("    ["+doc.qualifiedName()+"]");
		
		ClassConfiguration config = (ClassConfiguration)configurations.get(doc.qualifiedName());
		
		//output local methods
		if (!noLocalMethods){
			MethodDoc[] methods = doc.methods();
			ClassDoc[] interfaces = doc.interfaces();
			
			for (int i = 0 ; i< methods.length; i++){
				//System.out.println(methods[i].signature());
				if (config==null || config.isMethodAllowed(methods[i]))
					processMethodDoc(methods[i], overidenMethods, interfaces, currentDoc);
			}
		}
		
		//recursively process super classes for inherited methods. noLocalMethods flag 
		//must be turned off or inherited methods would not be output.
		if (!noInheritedMethods){
			boolean temp = noLocalMethods;
			if (noLocalMethods)
				noLocalMethods = false;
			
			processClassDoc(doc.superclass(), overidenMethods, currentDoc);
			
			noLocalMethods = temp;
		}
	}
	
	/**
	 * Processes a method doc.
	 * 
	 * @param doc				the method doc to be processed
	 * @param overidenMethods	all overiden methods so far
	 * @param interfaces		all interfaces of the classes containing given method
	 * @param currentDoc		the doc that is currently being processed
	 */
	private void processMethodDoc(	MethodDoc doc, 
									HashSet overidenMethods, 
									ClassDoc[] interfaces, 
									ClassDoc currentDoc)
	{
		if (! doc.isPublic()) return;
		
		//if the method is overriden, do not output it.
		if (overidenMethods.contains(doc))
			return;

		//if the method overrides some methods, record all overriden methods 
		if (doc.overriddenMethod()!=null){
			MethodDoc cdoc = doc;
			while (cdoc!=null){
				overidenMethods.add(cdoc);
				cdoc = cdoc.overriddenMethod();				
			}
			//interfaces methods are also recognized as overiden
			cdoc = getImplementMethodFromInterfaces(doc, interfaces);
			if (cdoc!=null) overidenMethods.add(cdoc);
		}
		
		
		if (!noComment){
			String commentText = processComments(doc).trim();
			//if the comment text is empty and overides a method, get the overriden 
			//method's comments or implemented  interface method's comment
			if (commentText.equals("")){
				MethodDoc cdoc = doc.overriddenMethod();
				while (cdoc!=null && commentText.equals("")){
					commentText = processComments(cdoc).trim();
					cdoc = cdoc.overriddenMethod();
				}
				if (commentText.equals("")){
					cdoc = getImplementMethodFromInterfaces(doc, interfaces);
					if (cdoc!=null)
						commentText = processComments(cdoc).trim();
				}
			}
			//show inheritance source
			//if the class containing this method is not the currently processed
			//class(currentDoc), this is a inherited method
			if (showInheritance && doc.containingClass()!=currentDoc){
				if (!commentText.equals("")){
					if (this.noHTMLTag)
						commentText = commentText +"\n\t";
					else
						commentText = commentText +"\n\t<br>";
				}
				if (this.noHTMLTag)
					commentText = commentText+"Inherited from "+doc.containingClass().qualifiedName();
				else
					commentText = commentText+"<i>Inherited from "+doc.containingClass().qualifiedName()+"</i>";
			}
			
			if (! commentText.equals("")){
				writer.println("\t/**");
				writer.println("\t"+commentText);
				writer.println("\t*/");
			}
		}
		
		writer.print("\tmethod ");
		if (doc.isStatic()) writer.print("static ");
		//writer.print(doc.returnType().qualifiedTypeName()+doc.returnType().dimension()+" ");
		writer.print(doc.returnType().qualifiedTypeName()+" ");
		writer.print(doc.name());
		writer.print("(");
		Parameter[] params = doc.parameters();
		for (int i=0;i<params.length;i++){
			if (params[i].name().equals(""))
				//writer.print(params[i].type().qualifiedTypeName()+params[i].type().dimension()+" arg"+i);
				writer.print(params[i].type().qualifiedTypeName()+" arg"+i);
			else
				//writer.print(params[i].type().qualifiedTypeName()+params[i].type().dimension()+" "+params[i].name());
				writer.print(params[i].type().qualifiedTypeName()+" "+params[i].name());
			if (i<params.length-1)
				//writer.print(", ");
				writer.print(",");
		}
		writer.print(")\n");
		writer.print("\t\n");
	}
	
	/**
	 * Processes comments. This method removes all javadoc links and remove html tags
	 * if required.
	 * 
	 * @param doc	the specified doc
	 * @return	a string of comment
	 */
	private String processComments(Doc doc){
		String result = "";
		Tag[] tags= doc.inlineTags();
		for (int i =0;i< tags.length;i++){
			result = result+tags[i].text();
		}
		if (this.noHTMLTag) result = removeHTMLTags(result);
		return result;
	}
	
	/**
	 * For a given method, finds if it is defined in the given set of interfaces.
	 * This method is required since implmented methods do not overides anything,
	 * you have to search the original definition using this method. 
	 * 
	 * @param method		the specified method
	 * @param interfaces	a set of interfaces
	 * @return	the method in givne interfaces that given method implements, null if none. 
	 */
	private MethodDoc getImplementMethodFromInterfaces(MethodDoc method, ClassDoc[] interfaces){
		
		for (int i=0;i<interfaces.length;i++){
			MethodDoc[] mdocs = interfaces[i].methods();
			for (int j=0;j<mdocs.length;j++){
				//compare method name
				if (method.name().equals(mdocs[j].name())){
					//compare parameter length
					if (method.parameters().length == mdocs[j].parameters().length){
						//compare params
//						//System.out.println(mdocs[j].signature());
						if (method.signature().equals(mdocs[j].signature())){
							//compare return type
							if (method.returnType().qualifiedTypeName().equals(mdocs[j].returnType().qualifiedTypeName())){
								if (method.returnType().dimension().equals(mdocs[j].returnType().dimension()))
									return mdocs[j];
							}
						}
					}
				}
			}
		}
		return null;
	}
	
	/**
	 * Removes all html tags from given string.
	 * 
	 * @param str	the given string
	 * @return	the result
	 */
	private String removeHTMLTags(String str){	
		EditorKit kit = new HTMLEditorKit();
		Document doc = kit.createDefaultDocument();
 
		// The Document class does not yet handle charset's properly.
		doc.putProperty("IgnoreCharsetDirective", Boolean.TRUE);
 
		// Create a reader on the HTML content.
		StringReader rd = new StringReader(str);
 
		// Parse the HTML.
		try {
			kit.read(rd, doc, 0);
		} catch (IOException e) {
			e.printStackTrace();
			return null;
		} catch (BadLocationException e) {
			e.printStackTrace();
			return null;
		}
 
		//  The HTML text is now stored in the document
		try {
			return doc.getText(0, doc.getLength());
		} catch (BadLocationException e) {
			e.printStackTrace();
			return null;
		}
	}
	
	private int loadConfigureFile(BufferedReader reader){
		String line="";
		boolean inClass=false;
		ClassConfiguration config=null;
		int lineno=0;
		
		try {
			while ((line = reader.readLine())!=null){
				lineno++;
				line = line.trim();
				if (line.startsWith("class ")){
					if (!inClass){
//						System.out.println("`"+line+"`");
//						System.out.println("`"+line.indexOf(' ')+1+"`");
//						System.out.println("`"+line.substring(line.indexOf(' '+1))+"`");
						config = new ClassConfiguration(line.substring(line.indexOf(" ")+1));
						inClass = true;
					}else{
						throw new Exception("wrong configuration file format 1");
					}
				}else if (line.equals("endclass")){
					if (inClass){
						inClass = false;
						if (config!=null)
							configurations.put(config.getClassDoc().qualifiedName(), config);
					}else throw new Exception("wrong configuration file format 2");
				}else if (line.equals("disallow:")){
					if (inClass) config.setAllowed(false);
					else throw new Exception("wrong configuration file format 3");
				}else if (line.equals("allow:")){
					if (inClass) config.setAllowed(true);
					else throw new Exception("wrong configuration file format 4");
				}else{
					if (inClass) {
						if (config.addMethod(line)) System.out.println("succcessfully load method "+line);
						else throw new Exception("failed at line "+lineno+": "+line);
					}
					//else throw new Exception("wrong configuration file format");
				}
			}
		} catch (Exception e) {
			System.out.println("error in configuration file line "+lineno+": `"+line+"`");
			e.printStackTrace();
			//System.exit(0);
			return -1;
		}
		return 0;
	}
	
	/*
	 *  (non-Javadoc)
	 * @see com.sun.javadoc.Doclet#start(com.sun.javadoc.RootDoc)
	 */
	public static boolean start(RootDoc root) {
		ClassSpecificationDoclet doclet = new ClassSpecificationDoclet(root);
		return doclet.outputSpecificationFiles();
	}
	
	/*
	 *  (non-Javadoc)
	 * @see com.sun.javadoc.Doclet#validOptions(java.lang.String[][], com.sun.javadoc.DocErrorReporter)
	 */
	public static boolean validOptions(String options[][], DocErrorReporter reporter){
		
		return true;
	}
	
	/*
	 *  (non-Javadoc)
	 * @see com.sun.javadoc.Doclet#optionLength(java.lang.String)
	 */
	public static int optionLength(String option){
		if (option.equals("-d")) return 2;
//		if (option.equals("-docfilessubdirs")) return 1;
		if (option.equals("-docfile")) return 2;
		if (option.equals("-noinheritedmethods")) return 1;
		if (option.equals("-inheritedmethodsonly")) return 1;
		if (option.equals("-showinheritancesource")) return 1;
		if (option.equals("-nohtmltag")) return 1;
		if (option.equals("-classconfig")) return 2;
		if (option.equals("-outputpackage")) return 2;
		if (option.equals("-nocomment")) return 1;
		return 0;
	} 
	 
	/**
	 * the root doc
	 */
	private RootDoc root;
	/**
	 * writer of current output file
	 */
	private PrintWriter writer;
	/**
	 * output root path
	 */
	private String outputRoot;
	
	private String outputFilename;
	
	private HashMap configurations;
	
	// see constructors for interpretations for following tags
	
	private boolean usePackageAsFolder;
	
	private boolean noInheritedMethods;
	
	private boolean noLocalMethods;
	
	private boolean showInheritance;
	
	private boolean noHTMLTag;
	
	private boolean noComment;
	
	private ArrayList packages;
	
	private class ClassConfiguration{
		public ClassConfiguration(String classQualifyName){
			doc = root.classNamed(classQualifyName);
			allowed = false;
			allowedIsSet = false;
			methodSet=new HashSet();
		}
		
		public ClassDoc getClassDoc(){
			return this.doc;
		}
		
		public void setAllowed(boolean allowed){
			if (allowedIsSet) return;
			
			this.allowed = allowed;
			this.allowedIsSet = true;
		}
		
		public boolean addMethod(String methodDesc){
			if (methodDesc.equals("*")){
				always = true;
				return true;
			}
			
			if (methodDesc==null || methodDesc.trim().equals("")) return true;
			
			String returnType=null;
			String methodName=null;
			String signature=null;
			
			StringTokenizer st1 = new StringTokenizer(methodDesc.trim(),"(), ");
			while (st1.hasMoreTokens()){
				returnType = st1.nextToken();
				if (!returnType.trim().equals("")) break;
			}
			while (st1.hasMoreTokens()){
				methodName = st1.nextToken();
				if (!methodName.trim().equals("")) break;
			}
			
			String temp = methodDesc.substring(methodDesc.indexOf('(')+1, methodDesc.indexOf(')'));
			st1 = new StringTokenizer(temp, ",");
			while (st1.hasMoreTokens()){
				temp = st1.nextToken().trim();
				if (!temp.equals("")){
					if (signature == null) 
						signature=temp.indexOf(' ')>0 ? temp.substring(0, temp.indexOf(' ')):temp;
					else 
						signature=signature+", "+(temp.indexOf(' ')>0 ? temp.substring(0, temp.indexOf(' ')):temp);
				}
			}
			
			if (signature==null) signature="";
			signature="("+signature+")";
			
			//System.out.println("-->\n`"+returnType+"` `"+methodName+"` `"+signature+"`");
			
			MethodDoc[] methods = doc.methods();
			for (int i=0;i<methods.length;i++){
				if (methods[i].name().equals(methodName)){
					//System.out.println("  name ok");
					//System.out.println("[sig]: `"+methods[i].signature()+"` `"+signature+"`");
					if (methods[i].signature().equals(signature)){
						//System.out.println("  signature ok");
						//System.out.println("[return]: `"+methods[i].returnType()+"` `"+returnType+"` "+methods[i].returnType().equals(returnType));
						if (methods[i].returnType().qualifiedTypeName().equals(returnType)){
							//System.out.println("  return type ok");
							methodSet.add(methods[i]);
							return true;
						}
					}
				}
			}
			return false;
		}
		
		public boolean isMethodAllowed(MethodDoc method){
			if (always) return allowed;
			
			if (allowed){
				if (methodSet.contains(method)) return true;
				else return false;
			}else{
				if (methodSet.contains(method)) return false;
				else return true;
			}
		}
		
		private ClassDoc doc;
		
		private boolean allowedIsSet;
		
		private boolean allowed;
		
		private boolean always;
		
		private HashSet methodSet;
		
	}
	
}
